/**
 * @author mrdoob / http://mrdoob.com/
 */

import {
	BackSide,
	DoubleSide,
	CubeUVRefractionMapping,
	CubeUVReflectionMapping,
	GammaEncoding,
	LinearEncoding,
	ObjectSpaceNormalMap,
} from '../../constants.js'
import {WebGLProgram} from './WebGLProgram.js'

function WebGLPrograms(renderer, extensions, capabilities, textures) {
	const programs = []

	const shaderIDs = {
		MeshDepthMaterial: 'depth',
		MeshDistanceMaterial: 'distanceRGBA',
		MeshNormalMaterial: 'normal',
		MeshBasicMaterial: 'basic',
		MeshLambertMaterial: 'lambert',
		MeshPhongMaterial: 'phong',
		MeshToonMaterial: 'phong',
		MeshStandardMaterial: 'physical',
		MeshPhysicalMaterial: 'physical',
		MeshMatcapMaterial: 'matcap',
		LineBasicMaterial: 'basic',
		LineDashedMaterial: 'dashed',
		PointsMaterial: 'points',
		ShadowMaterial: 'shadow',
		SpriteMaterial: 'sprite',
	}

	const parameterNames = [
		'precision',
		'supportsVertexTextures',
		'map',
		'mapEncoding',
		'matcap',
		'matcapEncoding',
		'envMap',
		'envMapMode',
		'envMapEncoding',
		'lightMap',
		'aoMap',
		'emissiveMap',
		'emissiveMapEncoding',
		'bumpMap',
		'normalMap',
		'objectSpaceNormalMap',
		'displacementMap',
		'specularMap',
		'roughnessMap',
		'metalnessMap',
		'gradientMap',
		'alphaMap',
		'combine',
		'vertexColors',
		'vertexTangents',
		'fog',
		'useFog',
		'fogExp',
		'flatShading',
		'sizeAttenuation',
		'logarithmicDepthBuffer',
		'skinning',
		'maxBones',
		'useVertexTexture',
		'morphTargets',
		'morphNormals',
		'maxMorphTargets',
		'maxMorphNormals',
		'premultipliedAlpha',
		'numDirLights',
		'numPointLights',
		'numSpotLights',
		'numHemiLights',
		'numRectAreaLights',
		'shadowMapEnabled',
		'shadowMapType',
		'toneMapping',
		'physicallyCorrectLights',
		'alphaTest',
		'doubleSided',
		'flipSided',
		'numClippingPlanes',
		'numClipIntersection',
		'depthPacking',
		'dithering',
	]

	function allocateBones(object) {
		const skeleton = object.skeleton
		const bones = skeleton.bones

		if (capabilities.floatVertexTextures) {
			return 1024
		} else {
			// default for when object is not specified
			// ( for example when prebuilding shader to be used with multiple objects )
			//
			//  - leave some extra space for other uniforms
			//  - limit here is ANGLE's 254 max uniform vectors
			//    (up to 54 should be safe)

			const nVertexUniforms = capabilities.maxVertexUniforms
			const nVertexMatrices = Mathf.floor((nVertexUniforms - 20) / 4)

			const maxBones = Mathf.min(nVertexMatrices, bones.length)

			if (maxBones < bones.length) {
				console.warn(
					'THREE.WebGLRenderer: Skeleton has ' + bones.length + ' bones. This GPU supports ' + maxBones + '.'
				)
				return 0
			}

			return maxBones
		}
	}

	function getTextureEncodingFromMap(map, gammaOverrideLinear) {
		let encoding

		if (!map) {
			encoding = LinearEncoding
		} else if (map.isTexture) {
			encoding = map.encoding
		} else if (map.isWebGLRenderTarget) {
			console.warn(
				"THREE.WebGLPrograms.getTextureEncodingFromMap: don't use render targets as textures. Use their .texture property instead."
			)
			encoding = map.texture.encoding
		}

		// add backwards compatibility for WebGLRenderer.gammaInput/gammaOutput parameter, should probably be removed at some point.
		if (encoding === LinearEncoding && gammaOverrideLinear) {
			encoding = GammaEncoding
		}

		return encoding
	}

	this.getParameters = function(material, lights, shadows, fog, nClipPlanes, nClipIntersection, object) {
		const shaderID = shaderIDs[material.type]

		// heuristics to create shader parameters according to lights in the scene
		// (not to blow over maxLights budget)

		const maxBones = object.isSkinnedMesh ? allocateBones(object) : 0
		const precision = capabilities.precision

		if (material.precision !== null) {
			precision = capabilities.getMaxPrecision(material.precision)

			if (precision !== material.precision) {
				console.warn(
					'THREE.WebGLProgram.getParameters:',
					material.precision,
					'not supported, using',
					precision,
					'instead.'
				)
			}
		}

		const currentRenderTarget = renderer.getRenderTarget()

		const parameters = {
			shaderID: shaderID,

			precision: precision,
			supportsVertexTextures: capabilities.vertexTextures,
			outputEncoding: getTextureEncodingFromMap(
				!currentRenderTarget ? null : currentRenderTarget.texture,
				renderer.gammaOutput
			),
			map: !!material.map,
			mapEncoding: getTextureEncodingFromMap(material.map, renderer.gammaInput),
			matcap: !!material.matcap,
			matcapEncoding: getTextureEncodingFromMap(material.matcap, renderer.gammaInput),
			envMap: !!material.envMap,
			envMapMode: material.envMap && material.envMap.mapping,
			envMapEncoding: getTextureEncodingFromMap(material.envMap, renderer.gammaInput),
			envMapCubeUV:
				!!material.envMap &&
				(material.envMap.mapping === CubeUVReflectionMapping ||
					material.envMap.mapping === CubeUVRefractionMapping),
			lightMap: !!material.lightMap,
			aoMap: !!material.aoMap,
			emissiveMap: !!material.emissiveMap,
			emissiveMapEncoding: getTextureEncodingFromMap(material.emissiveMap, renderer.gammaInput),
			bumpMap: !!material.bumpMap,
			normalMap: !!material.normalMap,
			objectSpaceNormalMap: material.normalMapType === ObjectSpaceNormalMap,
			displacementMap: !!material.displacementMap,
			roughnessMap: !!material.roughnessMap,
			metalnessMap: !!material.metalnessMap,
			specularMap: !!material.specularMap,
			alphaMap: !!material.alphaMap,

			gradientMap: !!material.gradientMap,

			combine: material.combine,

			vertexTangents: material.normalMap && material.vertexTangents,
			vertexColors: material.vertexColors,

			fog: !!fog,
			useFog: material.fog,
			fogExp: fog && fog.isFogExp2,

			flatShading: material.flatShading,

			sizeAttenuation: material.sizeAttenuation,
			logarithmicDepthBuffer: capabilities.logarithmicDepthBuffer,

			skinning: material.skinning && maxBones > 0,
			maxBones: maxBones,
			useVertexTexture: capabilities.floatVertexTextures,

			morphTargets: material.morphTargets,
			morphNormals: material.morphNormals,
			maxMorphTargets: renderer.maxMorphTargets,
			maxMorphNormals: renderer.maxMorphNormals,

			numDirLights: lights.directional.length,
			numPointLights: lights.point.length,
			numSpotLights: lights.spot.length,
			numRectAreaLights: lights.rectArea.length,
			numHemiLights: lights.hemi.length,

			numClippingPlanes: nClipPlanes,
			numClipIntersection: nClipIntersection,

			dithering: material.dithering,

			shadowMapEnabled: renderer.shadowMap.enabled && object.receiveShadow && shadows.length > 0,
			shadowMapType: renderer.shadowMap.type,

			toneMapping: renderer.toneMapping,
			physicallyCorrectLights: renderer.physicallyCorrectLights,

			premultipliedAlpha: material.premultipliedAlpha,

			alphaTest: material.alphaTest,
			doubleSided: material.side === DoubleSide,
			flipSided: material.side === BackSide,

			depthPacking: material.depthPacking !== undefined ? material.depthPacking : false,
		}

		return parameters
	}

	this.getProgramCode = function(material, parameters) {
		const array = []

		if (parameters.shaderID) {
			array.push(parameters.shaderID)
		} else {
			array.push(material.fragmentShader)
			array.push(material.vertexShader)
		}

		if (material.defines !== undefined) {
			for (let name in material.defines) {
				array.push(name)
				array.push(material.defines[name])
			}
		}

		for (let i = 0; i < parameterNames.length; i++) {
			array.push(parameters[parameterNames[i]])
		}

		array.push(material.onBeforeCompile.toString())

		array.push(renderer.gammaOutput)

		array.push(renderer.gammaFactor)

		return array.join()
	}

	this.acquireProgram = function(material, shader, parameters, code) {
		let program

		// Check if code has been already compiled
		for (let p = 0, pl = programs.length; p < pl; p++) {
			const programInfo = programs[p]

			if (programInfo.code === code) {
				program = programInfo
				++program.usedTimes

				break
			}
		}

		if (program === undefined) {
			program = new WebGLProgram(renderer, extensions, code, material, shader, parameters, capabilities, textures)
			programs.push(program)
		}

		return program
	}

	this.releaseProgram = function(program) {
		if (--program.usedTimes === 0) {
			// Remove from unordered set
			const i = programs.indexOf(program)
			programs[i] = programs[programs.length - 1]
			programs.pop()

			// Free WebGL resources
			program.destroy()
		}
	}

	// Exposed for resource monitoring & error feedback via renderer.info:
	this.programs = programs
}

export {WebGLPrograms}
